package org.tigris.atlas.persist.hibernate;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang.enums.Enum;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.usertype.UserType;

public abstract class AbstractEnumerationUserType implements UserType {
	
	private static Map ENUMS;
	private static final Log LOG = LogFactory.getLog(AbstractEnumerationUserType.class);
	
	private Class enumerationClass;
	
	/**
	 * Workaround for classloading issues where types were being used
	 * before the actual enumeration class has been loaded.  This method
	 * calls a static method on the enum class, and will ensure that
	 * enumerations have been loaded by the time a query using the
	 * user type class is run.
	 *
	 * @param clazz The class of the enumeration being registered
	 */
	private static synchronized void registerEnumeration(Class clazz) {
		if (ENUMS == null) {
			ENUMS = new HashMap();
		}
		
		String name = clazz.getName();
		Object value = ENUMS.get(name);
		if (value == null) {
			try {
				Method m = clazz.getMethod("getEnumMap", new Class [] {});
				Map map = (Map) m.invoke(clazz, new Object [] {});
				int size = map.size();
				ENUMS.put(name, new Integer(size));
				if (LOG.isDebugEnabled()) {
					LOG.debug("Registered enumeration class " + name + " with " + size + " enumerated value(s)");
				}
			} catch (Exception ex) {
				String errMsg = "Unable to register enumeration class " + name;
				LOG.error(errMsg, ex);
				throw new RuntimeException(errMsg, ex);
			}
		}
	}
	
	public AbstractEnumerationUserType(Class enumerationClass) {
        super();
		this.enumerationClass = enumerationClass;
		// Needed to address issue #74. See method JavaDocs for details.
		registerEnumeration(this.enumerationClass);
    }
	
	protected Class getEnumerationClass() {
		return this.enumerationClass;
	}

	/**
	 * @see org.hibernate.usertype.UserType#returnedClass()
	 */
	public Class returnedClass() {
		return enumerationClass;
	}

	/**
	 * @see org.hibernate.usertype.UserType#equals(java.lang.Object, java.lang.Object)
	 */
	public boolean equals(Object object1, Object object2) throws HibernateException {
	    if (object1 == object2) {
	        return true;
	    }
	    
	    if (object1 == null || object2 == null) {
	        return false;
	    }
	    
	    return Hibernate.STRING.isEqual(object1, object2);
	
	}

	/**
	 * @see org.hibernate.usertype.UserType#nullSafeGet(java.sql.ResultSet, java.lang.String[], java.lang.Object)
	 */
	public Object nullSafeGet(ResultSet resultSet, String[] names, Object object) throws HibernateException, SQLException {
		Enum e = null;
		String name = (names != null) ? names[0] : null;
		if ((name != null) && (resultSet != null)) { 
			String enumerationCode = (String) Hibernate.STRING.nullSafeGet(resultSet, name);
			if (enumerationCode != null) {
				e = resultSet.wasNull() ? null : getEnum(enumerationCode);
			}
		}
		return e;
	
	}

	protected abstract Enum getEnum(String enumerationCode);
	
	/**
	 * @see org.hibernate.usertype.UserType#nullSafeSet(java.sql.PreparedStatement, java.lang.Object, int)
	 */
	public void nullSafeSet(PreparedStatement ps, Object value, int index) throws HibernateException, SQLException {
		//verify type:
		Class valueClass = (value != null) ? value.getClass() : null;
		if ((valueClass != null) && (returnedClass() != null)) {
	        if (!returnedClass().isAssignableFrom(valueClass)) {
	            throw new IllegalArgumentException("Received value is not a '" + 
	            		returnedClass().getName() + "' but is rather a '" + value.getClass() + "'!");
	        }
		}
        
        setValue(ps, value, index);

	}	
	
	protected abstract void setValue(PreparedStatement ps, Object value, int index) throws SQLException;

	/**
	 * @see org.hibernate.usertype.UserType#deepCopy(java.lang.Object)
	 */
	public Object deepCopy(Object valueToCopy) throws HibernateException {
		//immutable value, nothing to do:
		return valueToCopy;
	}

	/**
	 * @see org.hibernate.usertype.UserType#isMutable()
	 */
	public boolean isMutable() {
		//immutable:
		return false;
	}

	/**
	 * @see org.hibernate.usertype.UserType#replace(java.lang.Object, java.lang.Object, java.lang.Object)
	 */
	public Object replace(Object original, Object target, Object owner) throws HibernateException {
		return original;
	}

	/**
	 * @see org.hibernate.usertype.UserType#assemble(java.io.Serializable, java.lang.Object)
	 */
	public Object assemble(Serializable cachedObject, Object owner) throws HibernateException {
		return (Serializable)deepCopy(cachedObject);
	}

	/**
	 * @see org.hibernate.usertype.UserType#disassemble(java.lang.Object)
	 */
	public Serializable disassemble(Object object) throws HibernateException {
		return (Serializable)deepCopy(object);
	}

	/**
	 * @see org.hibernate.usertype.UserType#hashCode(java.lang.Object)
	 */
	public int hashCode(Object x) throws HibernateException {
		return x.hashCode();
	}
	
	

}
